package com.decent.sms.service.impl;

import com.decent.sms.configs.IAcsConfig;
import com.decent.sms.dao.SmsBlacklistDao;
import com.decent.sms.entity.IAcsBean;
import com.decent.sms.entity.bo.SendSmsBO;
import com.decent.sms.enums.SmsTypeEnum;
import com.decent.sms.service.SmsConfigService;
import com.decent.sms.service.SmsService;
import com.decent.sms.utils.SendSmsUtils;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.aliyuncs.exceptions.ClientException;
import com.decent.common.bo.SimpleMessage;
import com.decent.common.cache.constant.RedisConstant;
import com.decent.common.constant.DatePatternConstant;
import com.decent.common.entity.sms.SmsBlacklist;
import com.decent.common.enums.ErrorCodeEnum;
import com.decent.common.enums.sms.SmsConfigKeyEnum;
import com.decent.common.enums.sms.SmsErrorCodeEnum;
import com.decent.common.exception.ErrorCodeException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicInteger;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 短信服务管理
 *
 * @author wangyx
 */
@Service
public class SmsServiceImpl implements SmsService {
    /**
     * 短信redis标识
     * redis键 = 手机号 + sendMessageType + 短信类型（type）
     */
    public static final String SEND_MESSAGE_TYPE = "sendMessageType";
    /**
     * 短信倒计时redis标识
     * redis键 =手机号 + sendMessageType + smsColdDown + 短信类型（type）
     */
    private static final String SMS_COLD_DOWN = "smsColdDown";
    /**
     * 每分钟同一ip发送短信次数
     */
    private static final Long IP_MINUTE_COUNT = 3L;
    /**
     * 每天同一ip发送短信次数
     */
    private static final Long IP_DAY_COUNT = 10L;
    /**
     * 短信倒计时（秒）
     */
    private static final Long SMS_COOLING = 60L;
    /**
     * 短信有效期（分钟）
     */
    private static final Long SMS_EFFECT_TIME = 10L;
    @Resource
    private SmsBlacklistDao smsBlacklistDao;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private IAcsConfig iAcsConfig;
    @Resource
    private SmsConfigService smsConfigService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 滑块验证码校验并发送短信
     *
     * @param telephone 手机号
     * @param type      短信类型
     * @param ip        操作ip
     * @param bean      阿里滑块相关信息
     * @return SimpleMessage
     * @throws ClientException 阿里滑块验证异常
     */
    @Override
    public SimpleMessage sendIacsCode(String telephone, SmsTypeEnum type, String ip, IAcsBean bean) throws ClientException {
        // TODO-INIT 按业务查询用户等信息
        Object user = new Object();
        // 注册操作时，根据账号查询到用户已存在则直接返回
        if (SmsTypeEnum.REGISTER == type && user != null) {
            return new SimpleMessage(SmsErrorCodeEnum.MEMBER_EXISTS);
        }
        // 非注册操作，需要有用户数据
        if (SmsTypeEnum.VERIFY == type && user == null) {
            return new SimpleMessage(SmsErrorCodeEnum.NO_MEMBER_EXISTS);
        }
        //黑名单校验
        SmsBlacklist blackList = smsBlacklistDao.queryBlackList(ip);
        if (blackList != null) {
            return new SimpleMessage(SmsErrorCodeEnum.VERIFY_CODE_FREQUENT);
        }
        // 判断短信倒计时是否结束
        String smsColdDownKey = getRedisKeyForColdDown(telephone, type);
        Boolean stillCooling = redisTemplate.opsForValue().setIfAbsent(smsColdDownKey, RedisConstant.COMMON_VALUE);
        if (stillCooling != null && !stillCooling) {
            return new SimpleMessage(SmsErrorCodeEnum.VERIFY_CODE_FREQUENT);
        }
        // 潜规则：等待短信倒计时期间不能重复点击获取短信
        redisTemplate.expire(smsColdDownKey, SMS_COOLING, TimeUnit.SECONDS);
        // 以下从每分钟获取短信和每天获取短信两个维度，防止恶意刷短信的情况
        String ipDayKey = getSmsRedisIpCounterKey(ip, type, TimeUnit.DAYS);
        String ipKey = getSmsRedisIpCounterKey(ip, type, TimeUnit.MINUTES);
        RedisConnectionFactory factory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
        RedisAtomicInteger ipDayCounter = new RedisAtomicInteger(ipDayKey, factory);
        RedisAtomicInteger ipMinuteCounter = new RedisAtomicInteger(ipKey, factory);
        ipDayCounter.incrementAndGet();
        ipDayCounter.expire(1, TimeUnit.DAYS);
        ipMinuteCounter.incrementAndGet();
        ipMinuteCounter.expire(1, TimeUnit.MINUTES);
        if (ipDayCounter.get() > IP_DAY_COUNT || ipMinuteCounter.get() > IP_MINUTE_COUNT) {
            smsBlacklistDao.addBlackList(ip);
            return new SimpleMessage(SmsErrorCodeEnum.VERIFY_CODE_FREQUENT);
        }
        // 阿里滑动验证
        bean.verify(ip, iAcsConfig.getAppKey(), iAcsConfig.getIAcsClient());
        String randomCode = RandomUtil.randomNumbers(6);
        String telephoneKey = getTelephoneRedisKey(telephone, type);
        if (sendSuccess(telephone, smsColdDownKey, ipDayCounter, ipMinuteCounter, randomCode, telephoneKey)) {
            return new SimpleMessage(ErrorCodeEnum.OK);
        }
        return new SimpleMessage(SmsErrorCodeEnum.SMS_SEND_FAIL);
    }

    /**
     * 发送短信并简单校验是否发送成功
     *
     * @param telephone       手机号
     * @param smsColdDownKey  获取短信冷却倒计时redis键
     * @param ipDayCounter    获取短信日限制计数器
     * @param ipMinuteCounter 获取短信分钟限制计数器
     * @param randomCode      短信验证码
     * @param telephoneKey    获取短信手机号redis键
     * @return true：发送成功；false：发送失败；
     */
    private boolean sendSuccess(String telephone, String smsColdDownKey, RedisAtomicInteger ipDayCounter,
                                RedisAtomicInteger ipMinuteCounter, String randomCode, String telephoneKey) {
        try {
            List<SmsConfigKeyEnum> queryKeys = Collections.singletonList(SmsConfigKeyEnum.DEMO_TPL_ID);
            HashMap<SmsConfigKeyEnum, String> config = smsConfigService.batchQuery(queryKeys);
            SendSmsBO sms = SendSmsBO.builder().yzxAccount(config.get(SmsConfigKeyEnum.YZX_ACCOUNT))
                    .yzxSecret(config.get(SmsConfigKeyEnum.YZX_SECRET)).sendUrl(config.get(SmsConfigKeyEnum.SEND_SMS_URL))
                    .mobile(telephone).params("code:" + randomCode).templateId(config.get(SmsConfigKeyEnum.DEMO_TPL_ID)).build();
            SendSmsUtils.sendMsg(sms);
        } catch (Exception e) {
            // 发送失败，删除冷却时间、计数器还原
            redisTemplate.delete(smsColdDownKey);
            ipDayCounter.decrementAndGet();
            ipMinuteCounter.decrementAndGet();
            return false;
        }
        // 发送成功，保存映射
        stringRedisTemplate.opsForValue().set(telephoneKey, randomCode, SMS_EFFECT_TIME, TimeUnit.MINUTES);
        return true;
    }

    /**
     * 获取发送短信手机号的redis键，用于记录手机号和短信验证码的映射
     *
     * @param telephone 手机号
     * @param type      短信类型
     * @return redis键
     */
    private String getTelephoneRedisKey(String telephone, SmsTypeEnum type) {
        return telephone + SEND_MESSAGE_TYPE + type;
    }

    /**
     * 获取发送短信ip计数器的redis键
     *
     * @param ip       ip地址
     * @param type     短信类型
     * @param timeType 时间类型
     * @return redis键
     */
    private String getSmsRedisIpCounterKey(String ip, SmsTypeEnum type, TimeUnit timeType) {
        String format;
        switch (timeType) {
            case DAYS:
                format = LocalDateTimeUtil.format(LocalDate.now(), DatePatternConstant.NORM_DATE_PATTERN);
                break;
            case MINUTES:
                format = LocalDateTimeUtil.format(LocalDateTime.now(), DatePatternConstant.NORM_DATE_MINUTE_PATTERN);
                break;
            default:
                format = "";
        }
        return ip + SEND_MESSAGE_TYPE + format + type;
    }

    /**
     * 获取短信倒计时redis键
     *
     * @param telephone 手机号
     * @param type      短信类型
     * @return redis键
     */
    private String getRedisKeyForColdDown(String telephone, SmsTypeEnum type) {
        return telephone + SEND_MESSAGE_TYPE + SMS_COLD_DOWN + type;
    }

    /**
     * 短信验证码校验
     *
     * @param telephone  手机号
     * @param verifyCode 验证码
     * @param type       短信类型
     */
    @Override
    public void verifyCode(String telephone, String verifyCode, SmsTypeEnum type) throws ErrorCodeException {
        String telephoneKey = getTelephoneRedisKey(telephone, type);
        String redisValue = stringRedisTemplate.opsForValue().get(telephoneKey);
        if (ObjectUtil.isNull(redisValue)) {
            throw new ErrorCodeException(SmsErrorCodeEnum.NO_VERIFY_CODE_SEND);
        }
        if (!StrUtil.equals(redisValue, verifyCode)) {
            throw new ErrorCodeException(SmsErrorCodeEnum.INVALID_VERIFY_CODE);
        }
        redisTemplate.delete(telephoneKey);
    }
}
